#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <common/macro/build.h>
#include <common/macro/array.h>
#include <common/macro/debug.h>
#include <common/util.h>
#include <database/database.h>

#include <transaction/transaction.h>
#include <transaction/abortlog.h>
#include <transaction/transactionlog.h>

/** 
* @brief Open a database. Creates transaction log and abort log, if these
* did not exist.
* 
* @param basedir Directory which contains the database.
* 
* @return Pointer to database on success, NULL otherwise.
*/
API_PUBLIC database_t *database_open(const char *basedir) {
	char *fn;
	database_t *database;

	database = malloc(sizeof(*database));

	database->basedir = basedir;

	database->tables = NULL;
	database->tables_c = 0;

	fn = util_strmerge(database->basedir, "/transactionlog", NULL);
	database->transaction_log = transaction_log_open(fn);
	free(fn);
	if (database->transaction_log == NULL) {
		free(database);
		return NULL;
	}

	fn = util_strmerge(database->basedir, "/abortlog", NULL);
	database->abort_log = abort_log_open(fn);
	free(fn);
	if (database->abort_log == NULL) {
		transaction_log_close(database->transaction_log);
		free(database);
		return NULL;
	}


	return database;
}

/** 
* @brief Closes the database.
* 
* @param database
*/
API_PUBLIC void database_close(database_t *database) {
	int i;

	if (database == NULL) { return; }

	abort_log_close(database->abort_log);
	transaction_log_close(database->transaction_log);

	for (i = 0; i < database->tables_c; i++) {
		storage_close(database->tables[i].storage);
		table_destroy(database->tables[i].table);
	}
	free(database->tables);

	free(database);
}

/** 
* @brief Looks up a table in database.
* 
* @param database
* @param tablename
* @param table_storage Output argument.
* 
* @return Zero on non-existent table, one on a hit.
*/
API_FILELOCAL int database_table_lookup(database_t *database, const char *tablename, table_storage_t *table_storage) {
	int i;

	for (i = 0; i < database->tables_c; i++) {
		if (strcmp(database->tables[i].table->name, tablename) == 0) {
			*table_storage = database->tables[i];
			return 1;
		}
	}
	return 0;
}

/** 
* @brief Retrieves table from the database.
* 
* @param database
* @param tablename
* @param table_storage Output argument.
* 
* @return Zero on non-existent table, one on a hit.
*/
API_PUBLIC table_t *database_table_get(database_t *database, const char *tablename) {
	int rv;
	table_storage_t table_storage;

	rv = database_table_open(database, tablename, &table_storage);
	if (rv != 0)
		return NULL;
	return table_storage.table;
}

/** 
* @brief Creates a new table according to specification.
* 
* @param database
* @param table
* 
* @return Zero on success, -1 otherwise.
*/
API_PUBLIC int database_table_create(database_t *database, table_t *table) {
	int rv;
	char *fn;
	iohandle_t fp;

	assert(database != NULL);
	assert(table != NULL);

	fn = util_strmerge(database->basedir, "/", table->name, ".desc", NULL);
	fp = io_open_rwc(fn);
	free(fn);
	rv = table_serialize_file(table, fp);
	io_close(fp);

	return (rv == 0 ? 0 : -1);
}

/** 
* @brief Opens an existing table.
* 
* @param database
* @param tablename
* @param table_storage
* 
* @return Zero on success, -1 otherwise.
*/
API_PUBLIC int database_table_open(database_t *database, const char *tablename, table_storage_t *table_storage) {
	char *fn;
	iohandle_t fp;

	assert(database != NULL);
	assert(tablename != NULL);

	/* Perhaps it is already open? */
	if (database_table_lookup(database, tablename, table_storage))
		return 0;

	fn = util_strmerge(database->basedir, "/", tablename, NULL);
	table_storage->storage = storage_open(fn, 4096);
	free(fn);
	if (table_storage->storage == NULL) {
		return -1;
	}

	fn = util_strmerge(database->basedir, "/", tablename, ".desc", NULL);
	fp = io_open_r(fn);
	free(fn);
	if (!io_valid(fp))
		return -1;
	table_storage->table = table_deserialize_file(fp);
	io_close(fp);
	if (table_storage->table == NULL) {
		return -1;
	}

	ARRAY_APPEND(database->tables, database->tables_c, *table_storage);

	return 0;
}

/** 
* @brief Drop the given table. Other sessions that have this table open
* will continue as before, but the table files are already unlinked. New
* sessions will not be able to use the table.
* 
* @param database
* @param tablename
* 
* @return 
*/
API_PUBLIC int database_table_drop(database_t *database, const char *tablename) {
	int rv1, rv2;
	char *fn;
	table_storage_t table_storage;

	/* Close all table files in this session. */
	if (database_table_lookup(database, tablename, &table_storage)) {
		storage_close(table_storage.storage);
		table_destroy(table_storage.table);
	}

	/* Unlink the table files: */

	/*
	 * - Table description.
	 * It must go first, it is used to determine existence of the table. 
	 */
	fn = util_strmerge(database->basedir, "/", tablename, ".desc", NULL);
	rv1 = io_remove(fn);
	free(fn);
	
	/*
	 * - Data files.
	 * This means the pages and FSM files associated with the table.
	 */
	fn = util_strmerge(database->basedir, "/", tablename, NULL);
	rv2 = storage_remove(fn);
	free(fn);

	/* 
	 * We actually discard failures on storage removal,
	 * because the storage may not have been present at all.
	 */
	return rv1;
}

/** 
* @brief Open all available tables in the database.
* 
* @param database
* 
* @return Zero on success, -1 otherwise.
*/
API_LIBLOCAL int database_table_open_all(database_t *database) {
	int rv;
	DIR *tabledir;
	struct dirent *item;
	table_storage_t table_storage;
	size_t slen;
	
	tabledir = opendir(database->basedir);
	if (tabledir == NULL)
		return -1;

	while ((item = readdir(tabledir)) != NULL) {
		slen = strlen(item->d_name);
		if (strcmp(".desc", &item->d_name[slen - 5]) == 0) {
			item->d_name[slen - 5] = '\0';
			rv = database_table_open(database, item->d_name, &table_storage);
			if (rv != 0)
				return rv;
		}
	}
	closedir(tabledir);

	return 0;
}

/** 
* @brief Flush all data from all open tables to disc.
* 
* @param database 
* 
* @return Always zero, ie. success
*/
API_LIBLOCAL int database_fsync_data(database_t *database) {
	int i;

	for (i = 0; i < database->tables_c; i++) {
		storage_fsync(database->tables[i].storage);
	}

	return 0;
}
